package jp.dodododo.reloadable.serialization.impl;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import jp.dodododo.reloadable.serialization.SerializedObject;
import jp.dodododo.reloadable.util.SerializationUtils;

public class SerializedObjectImpl implements SerializedObject {

	private static final long serialVersionUID = 8683315989903185286L;

	private String originalClassName;

	private Map<String, SerializedObjectImpl> fieldValues;

	private List<SerializedObjectImpl> listValue;

	private Map<SerializedObjectImpl, SerializedObjectImpl> mapValues;

	private Serializable originalValue;

	private byte[] originalObjectByte;

	private boolean isArray;

	private int arrayLength;

	private static ThreadLocal<Map<Integer, SerializedObjectImpl>> serializedObjects = new ThreadLocal<Map<Integer, SerializedObjectImpl>>();

	private static ThreadLocal<Map<Integer, Object>> deserializedObjects = new ThreadLocal<Map<Integer, Object>>();

	public SerializedObjectImpl(Object src) {
		try {
			putSerializedObjects(System.identityHashCode(src), this);
			init(src);
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException("" + src, e);
		}
	}

	private void putSerializedObjects(int identityHashCode, SerializedObjectImpl object) {
		Map<Integer, SerializedObjectImpl> map = serializedObjects.get();
		if (map == null) {
			map = new HashMap<Integer, SerializedObjectImpl>();
			serializedObjects.set(map);
		}
		map.put(identityHashCode, object);
	}

	private void putDeserializedObjects(int identityHashCode, Object object) {
		Map<Integer, Object> map = deserializedObjects.get();
		if (map == null) {
			map = new HashMap<Integer, Object>();
			deserializedObjects.set(map);
		}
		map.put(identityHashCode, object);
	}

	/**
	 * @IgnorePoint
	 */
	@SuppressWarnings("unchecked")
	private void init(Object src) throws Exception {
		if (src == null) {
			return;
		}
		if (src instanceof Serializable) {
			originalValue = (Serializable) src;
			if (hasDefaultConstructor(src) == false) {
				try {
					originalObjectByte = SerializationUtils.serialize(originalValue);
				} catch (Exception ignore) {
				}
			}
		}
		Class<?> clazz = src.getClass();
		originalClassName = clazz.getName();

		if (isLangPackage(clazz) || classSet.contains(clazz) || clazz.isPrimitive()) {
			return;
		}
		originalValue = null;
		if (src instanceof Collection) {
			Collection originalList = (Collection) src;
			listValue = new ArrayList<SerializedObjectImpl>();
			for (Iterator iter = originalList.iterator(); iter.hasNext();) {
				Object element = iter.next();
				listValue.add(toSerializedObject(element));
			}
		} else if (src instanceof Map) {
			Map originalMap = new HashMap((Map) src);
			mapValues = new HashMap<SerializedObjectImpl, SerializedObjectImpl>();
			@SuppressWarnings("unchecked")
			Set<Entry> entrySet = originalMap.entrySet();
			for (Iterator<Entry> iter = entrySet.iterator(); iter.hasNext();) {
				Entry element = iter.next();
				mapValues.put(toSerializedObject(element.getKey()), toSerializedObject(element.getValue()));
			}
		} else if (clazz.isArray()) {
			isArray = true;
			originalClassName = clazz.getComponentType().getName();
			listValue = new ArrayList<SerializedObjectImpl>();
			arrayLength = Array.getLength(src);
			for (int i = 0; i < arrayLength; i++) {
				Object object = Array.get(src, i);
				listValue.add(toSerializedObject(object));
			}
		} else {
			List<Field> fields = getAllFields(clazz);
			fieldValues = new HashMap<String, SerializedObjectImpl>();
			for (Field field : fields) {
				field.setAccessible(true);
				if (isStatic(field)) {
					continue;
				}
				String fieldName = field.getDeclaringClass().getName() + "-" + field.getName();
				Object fieldValue = field.get(src);
				fieldValues.put(fieldName, toSerializedObject(fieldValue));
			}
		}
	}

	private boolean hasDefaultConstructor(Object src) throws SecurityException {
		try {
			Constructor<? extends Object> constructor = src.getClass().getConstructor((Class[]) null);
			if (constructor != null) {
				return true;
			}
			return false;
		} catch (NoSuchMethodException e) {
			return false;
		}
	}

	private List<Field> getAllFields(Class<?> target) {
		List<Field> ret = new ArrayList<Field>();
		addFields(target, ret);
		return ret;
	}

	private void addFields(Class<?> target, List<Field> list) {
		Field[] declaredFields = target.getDeclaredFields();
		for (Field f : declaredFields) {
			int modifiers = f.getModifiers();
			if (Modifier.isStatic(modifiers) == true || Modifier.isTransient(modifiers) == true) {
				continue;
			}
			list.add(f);
		}
		Class<?> superclass = target.getSuperclass();
		if (superclass == null || superclass.equals(Object.class)) {
			return;
		}
		addFields(superclass, list);
	}

	private boolean isStatic(Field field) {
		return Modifier.isStatic(field.getModifiers());
	}

	private boolean isLangPackage(Class<?> clazz) {
		String className = clazz.getName();
		if (className.startsWith("java.lang.")) {
			return true;
		}
		Package p = clazz.getPackage();
		if (p != null && "java.lang".equals(p.getName())) {
			return true;
		}
		return false;
	}

	/**
	 * @IgnorePoint
	 */
	public Object getObject() {
		return deserialize();
	}

	private Object newInstance() throws Exception {
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		Class<?> clazz = forName(originalClassName, loader);
		if (isArray == false) {
			try {
				return clazz.newInstance();
			} catch (Exception e) {
				if (originalObjectByte != null) {
					return SerializationUtils.deserialize(originalObjectByte, loader);
				} else {
					throw e;
				}
			}
		} else {
			return Array.newInstance(clazz, arrayLength);
		}
	}

	private Object deserialize() {
		Object object = null;
		try {
			if (this.fieldValues == null && this.listValue == null && this.mapValues == null && this.originalValue == null) {
				return null;
			}
			if (originalValue != null) {
				return originalValue;
			}
			ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
			Class<?> clazz = forName(originalClassName, contextClassLoader);
			object = newInstance();
			putDeserializedObjects(System.identityHashCode(this), object);
			if (object instanceof Collection) {
				@SuppressWarnings("unchecked")
				List<Object> retValue = (List<Object>) object;
				for (Iterator<SerializedObjectImpl> iter = listValue.iterator(); iter.hasNext();) {
					SerializedObjectImpl element = iter.next();
					try {
						retValue.add(toDeserializedObject(element));
					} catch (RuntimeException ignore) {
						// 型が変わっている場合とか
					}
				}
			} else if (object instanceof Map) {
				Set<Entry<SerializedObjectImpl, SerializedObjectImpl>> entrySet = mapValues.entrySet();
				@SuppressWarnings("unchecked")
				Map<Object, Object> retValue = (Map<Object, Object>) object;
				for (Iterator<Entry<SerializedObjectImpl, SerializedObjectImpl>> iter = entrySet.iterator(); iter.hasNext();) {
					Entry<SerializedObjectImpl, SerializedObjectImpl> element = iter.next();
					try {
						retValue.put(toDeserializedObject(element.getKey()), toDeserializedObject(element.getValue()));
					} catch (RuntimeException ignore) {
						// 型が変わっている場合とか
					}
				}
			} else if (isArray) {
				for (int i = 0; i < arrayLength; i++) {
					SerializedObjectImpl element = listValue.get(i);
					try {
						Array.set(object, i, toDeserializedObject(element));
					} catch (RuntimeException ignore) {
						// 型が変わっている場合とか
					}
				}
			} else {
				Set<Entry<String, SerializedObjectImpl>> entrySet = fieldValues.entrySet();
				for (Iterator<Entry<String, SerializedObjectImpl>> iter = entrySet.iterator(); iter.hasNext();) {
					Entry<String, SerializedObjectImpl> entry = iter.next();
					try {
						// Field field = clazz.getDeclaredField(entry.getKey());
						Field field = getField(clazz, entry.getKey());
						field.setAccessible(true);
						SerializedObjectImpl value = entry.getValue();
						field.set(object, toDeserializedObject(value));
					} catch (NoSuchFieldException ignore) {
					} catch (RuntimeException ignore) {
						// 型が変わっている場合とか
					}
				}
			}
			return object;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	private Field getField(Class<?> target, String name) throws NoSuchFieldException {
		String className = name.split("-")[0];
		String fieldName = name.split("-")[1];
		if (target == null || target.equals(Object.class)) {
			throw new NoSuchFieldException(fieldName);
		}
		try {
			Field declaredField = target.getDeclaredField(fieldName);
			if (declaredField.getDeclaringClass().getName().equals(className)) {
				return declaredField;
			} else {
				return getField(target.getSuperclass(), name);
			}
		} catch (NoSuchFieldException e) {
			return getField(target.getSuperclass(), name);
		}
	}

	private Class<?> forName(String className, ClassLoader contextClassLoader) throws ClassNotFoundException {
		if (primitiveClass.containsKey(className)) {
			return primitiveClass.get(className);
		}
		Class<?> clazz = Class.forName(className, true, contextClassLoader);
		return clazz;
	}

	private SerializedObjectImpl toSerializedObject(Object src) {
		Map<Integer, SerializedObjectImpl> map = serializedObjects.get();
		int identityHashCode = System.identityHashCode(src);
		if (map != null) {
			SerializedObjectImpl object = map.get(identityHashCode);
			if (object != null) {
				return object;
			}
		}
		return new SerializedObjectImpl(src);
	}

	private Object toDeserializedObject(SerializedObjectImpl src) {
		Map<Integer, Object> map = deserializedObjects.get();
		int identityHashCode = System.identityHashCode(src);
		if (map != null) {
			Object object = map.get(identityHashCode);
			if (object != null) {
				return object;
			}
		}
		return src.getObject();
	}

	private static Set<Class<?>> classSet = new HashSet<Class<?>>();
	static {
		classSet.add(java.io.File.class);
		classSet.add(java.math.BigDecimal.class);
		classSet.add(java.math.BigInteger.class);
		classSet.add(java.net.URI.class);
		classSet.add(java.net.URL.class);
		classSet.add(java.util.Date.class);
		classSet.add(java.util.Calendar.class);
		classSet.add(java.util.GregorianCalendar.class);
		classSet.add(java.util.Locale.class);
		classSet.add(java.util.Currency.class);
		classSet.add(java.util.TimeZone.class);
		classSet.add(java.util.UUID.class);
		classSet.add(java.sql.Date.class);
		classSet.add(java.sql.Time.class);
		classSet.add(java.sql.Timestamp.class);
	}

	private static Map<String, Class<?>> primitiveClass = new HashMap<String, Class<?>>();
	static {
		primitiveClass.put(Character.TYPE.getName(), Character.TYPE);
		primitiveClass.put(Byte.TYPE.getName(), Byte.TYPE);
		primitiveClass.put(Short.TYPE.getName(), Short.TYPE);
		primitiveClass.put(Integer.TYPE.getName(), Integer.TYPE);
		primitiveClass.put(Long.TYPE.getName(), Long.TYPE);
		primitiveClass.put(Float.TYPE.getName(), Float.TYPE);
		primitiveClass.put(Double.TYPE.getName(), Double.TYPE);
		primitiveClass.put(Void.TYPE.getName(), Void.TYPE);
		primitiveClass.put(Boolean.TYPE.getName(), Boolean.TYPE);
	}
}
